/*
 *    Geotoolkit - An Open Source Java GIS Toolkit
 *    http://www.geotoolkit.org
 *
 *    (C) 2010, Geomatys
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */

package org.geotoolkit.xml;

import java.io.*;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.Result;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.dom.DOMSource;
import org.w3c.dom.Node;

import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;

import static javax.xml.stream.XMLStreamReader.*;
import javax.xml.stream.util.StreamReaderDelegate;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.stax.StAXSource;
import org.geotoolkit.nio.IOUtilities;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

/**
 * An abstract class for all stax parser.<br/>
 * Readers for a given specification should extend this class and
 * provide appropriate read methods.<br/>
 * <br/>
 * Example : <br/>
 * <pre>
 * {@code
 * public class UserReader extends StaxStreamReader{
 *
 *   public User read() throws XMLStreamException{
 *      //casual stax reading operations
 *      return user;
 *   }
 * }
 * }
 * </pre>
 * And should be used like :<br/>
 * <pre>
 * {@code
 * final UserReader instance = new UserReader();
 * try{
 *     instance.setInput(stream);
 *     user = instance.read();
 * }finally{
 *     instance.dispose();
 * }
 * </pre>
 *
 * @author Johann Sorel (Geomatys)
 * @module
 */
public abstract class StaxStreamReader extends AbstractConfigurable {

    protected XMLStreamReader reader;

    /**
     * Store the input stream if it was generated by the parser itself.
     * It will closed on the dispose method or when a new input is set.
     */
    private InputStream sourceStream;

    public StaxStreamReader(){
    }

    /**
     * close potentiel previous stream and cache if there are some.
     * This way the reader can be reused for a different input later.
     * The underlying stax reader will be closed.
     */
    public void reset() throws IOException, XMLStreamException{
        if(sourceStream != null){
            sourceStream.close();
            sourceStream = null;
        }
        if(reader != null){
            reader.close();
            reader = null;
        }
    }

    /**
     * Release potentiel locks or opened stream.
     * Must be called when the reader is not needed anymore.
     * It should not be used after this method has been called.
     */
    public void dispose() throws IOException, XMLStreamException{
        reset();
    }

    /**
     * Set the input for this reader.<br/>
     * Handle types are :<br/>
     * - java.io.File<br/>
     * - java.io.Reader<br/>
     * - java.io.InputStream<br/>
     * - java.net.URL<br/>
     * - java.net.URI<br/>
     * - javax.xml.stream.XMLStreamReader<br/>
     * - javax.xml.transform.Source<br/>
     *
     * @param input
     * @throws IOException
     * @throws XMLStreamException
     */
    public void setInput(Object input) throws IOException, XMLStreamException{
        reset();

        if(input instanceof XMLStreamReader){
            reader = (XMLStreamReader) input;
            return;
        }

        if(input instanceof File){
            sourceStream = new FileInputStream((File)input);
            input = sourceStream;
        }else if(input instanceof Path){
            sourceStream = Files.newInputStream((Path)input, StandardOpenOption.READ);
            input = sourceStream;
        }else if(input instanceof URL){
            sourceStream = ((URL)input).openStream();
            input = sourceStream;
        }else if(input instanceof URI){
            sourceStream = IOUtilities.open(input);
            input = sourceStream;
        }else if(input instanceof String){
            input = new StringReader((String) input);
        }

        reader = toReader(input);
    }


    /**
     * Iterator on the reader until it reachs the end of the given tag name.
     * @param tagName tag name to search
     * @throws XMLStreamException
     */
    protected void toTagEnd(final String tagName) throws XMLStreamException{
        while (reader.hasNext()) {
            if(END_ELEMENT == reader.next() &&
               tagName.equalsIgnoreCase(reader.getLocalName()))
               return;
        }
        throw new XMLStreamException("Error in xml file, Could not find end of tag "+tagName+" .");
    }

    /**
     * Creates a new XMLStreamReader.
     * @param input
     * @return XMLStreamReader
     * @throws XMLStreamException if the input is not handled
     */
    private static final XMLStreamReader toReader(final Object input)
            throws XMLStreamException {
        final XMLInputFactory XMLfactory = XMLInputFactory.newInstance();
        XMLfactory.setProperty("http://java.sun.com/xml/stream/properties/report-cdata-event", Boolean.TRUE);
        XMLfactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.FALSE);

        if (input instanceof InputStream) {
            return XMLfactory.createXMLStreamReader((InputStream) input);
        } else if (input instanceof Source) {
            return XMLfactory.createXMLStreamReader((Source) input);
        } else if (input instanceof Node) {

            /* here we can think that we can use a DOMSource and pass it directly to the
             * method XMLfactory.createXMLStreamReader(Source) but it lead to a NPE
             * during the geometry unmarshall.
             */
            try {
                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                Result outputTarget = new StreamResult(outputStream);
                Transformer t = TransformerFactory.newInstance().newTransformer();
                t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
                t.transform(new DOMSource((Node) input), outputTarget);
                return XMLfactory.createXMLStreamReader(new ByteArrayInputStream(outputStream.toByteArray()));
            } catch (TransformerException ex) {
                throw new XMLStreamException(ex);
            }
        } else if (input instanceof Reader) {
            return XMLfactory.createXMLStreamReader((Reader) input);
        } else {
            throw new XMLStreamException("Input type is not supported : " + input);
        }
    }

    /**
     * <p>XML language provides two notations for boolean type :
     * "true" can be written "1" and "0" significates "false".
     * This method considers all this values as CharSequences and return its boolean value.</p>
     *
     * @param bool The String to parse
     * @return true if bool is equal to "true" or "1".
     */
    protected static boolean parseBoolean(final String candidate) {
        if (candidate.length() == 1) {
            return !candidate.equals("0");
        }
        return Boolean.parseBoolean(candidate);
    }

    /**
     * <p>This method reads doubles with coma separated.</p>
     *
     * @param candidate Can not be null.
     * @return
     */
    protected static double parseDouble(final String candidate) {
        return Double.parseDouble(candidate.replace(',', '.'));
    }

    /**
     * Iterator on the reader until it reachs the end of the given tag name.
     * Return the read elements as dom.
     *
     * TODO : not supported yet, needed for GML antype nodes, waiting for the new Feature Model.
     *
     * @return Element
     */
    protected Document readAsDom(final String tagName) throws XMLStreamException{

        final XMLStreamReader limitedReader = new StreamReaderDelegate(reader){
            boolean finished = false;

            @Override
            public boolean hasNext() throws XMLStreamException {
                if(finished) return false;
                return super.hasNext();
            }

            @Override
            public int next() throws XMLStreamException {
                int t = super.next();
                finished = END_ELEMENT == t && tagName.equalsIgnoreCase(reader.getLocalName());
                return t;
            }

        };

        final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(false);

        final TransformerFactory trsFactory = TransformerFactory.newInstance();
        try {
            final DocumentBuilder builder = factory.newDocumentBuilder();
            final Transformer idTransform = trsFactory.newTransformer();
            final Source input = new StAXSource(limitedReader);
            final ByteArrayOutputStream out = new ByteArrayOutputStream();
            final Result output = new StreamResult(out);
            idTransform.transform(input, output);
            final Document doc = builder.parse(new ByteArrayInputStream(out.toByteArray()));
            return doc;
        } catch (TransformerConfigurationException e) {
            throw new XMLStreamException(e.getMessage());
        } catch (TransformerFactoryConfigurationError e) {
            throw new XMLStreamException(e.getMessage());
        } catch (IOException e) {
            throw new XMLStreamException(e.getMessage());
        } catch (TransformerException e) {
            throw new XMLStreamException(e.getMessage());
        } catch (SAXException e) {
            throw new XMLStreamException(e.getMessage());
        } catch (ParserConfigurationException e) {
            throw new XMLStreamException(e.getMessage());
        }

    }

}
